/**
 * Copy right 2013 by Phan Hoàn Vũ
 * Email hoanvuphan@gmail.com
 */
package vn.universex.builder;

import japa.parser.ASTHelper;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.ImportDeclaration;
import japa.parser.ast.PackageDeclaration;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.ConstructorDeclaration;
import japa.parser.ast.body.JavadocComment;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.ModifierSet;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.body.TypeDeclaration;
import japa.parser.ast.expr.NameExpr;
import japa.parser.ast.expr.QualifiedNameExpr;
import japa.parser.ast.stmt.BlockStmt;
import japa.parser.ast.type.ClassOrInterfaceType;
import japa.parser.ast.type.ReferenceType;
import japa.parser.ast.type.Type;
import japa.parser.ast.visitor.VoidVisitorAdapter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;

/**
 * Implementation of {@link ClassBuilder}.
 * 
 * @author Phan Hoan Vu
 * @see ClassBuilder
 */
public class ClassBuilderImpl extends VoidVisitorAdapter<Object> implements ClassBuilder {

    private static final String DEFAULT_CLASS_PACKAGE = "vn.universex";

    /**
     * List of all java.lang type. I hope it's enough :) We will see if this class need to be extended in future.
     */
    private static final List<String> LIST_OF_JAVA_LANG_TYPE = Arrays.asList("String", "Integer", "Long", "Short",
            "Number", "Object", "Double", "Boolean");

    /**
     * List of name of collections type.
     */
    private static final List<String> LIST_OF_COLLECTION_TYPE = Arrays.asList("Set", "List");

    /**
     * The JavaDoc comment on the header of result.
     */
    private static final String CLASS_JAVADOC = "Generated by property-generator-maven-plugin";

    /**
     * The pattern to check whether a method is getter or not.
     */
    private static final Pattern GETTER_PATTERN = Pattern.compile("get([A-Z].?[a-zA-Z0-9].*)");

    /**
     * This field contains a string that's represented for the suffix of result class. Usually, the name of the result
     * class will be created by concatenating the old class name with this suffix.
     */
    private String resultSuffix;

    /**
     * A repository that contains all information about the package of old source code.
     * <p>
     * This data is used to check whether a package is belong to parsing process or not.
     */
    private Map<String, Set<String>> packageInfos;

    /**
     * This is technical field that's used to store the current generating source code.
     */
    private CompilationUnit toBeBuiltCompilationUnit;

    /**
     * This is technical field also that's used to store the current generating class.
     */
    private ClassOrInterfaceDeclaration toBeBuiltClass;

    /**
     * List of imports that's needed to be imported for the result class. :D
     * <p>
     * Sorry about this comment, but I'm so tired today.
     */
    private List<ImportDeclaration> toBeBuiltImports = new ArrayList<>();

    /**
     * The package name of source code.
     */
    private String sourcePackage;

    /**
     * List of all imports of source code.
     */
    private Map<String, String> sourceImports = new HashMap<>();

    /**
     * Constructor.
     * 
     * @param resultSuffix
     *            See --> {@link ClassBuilderImpl#resultSuffix}
     * @param packageInfos
     *            See --> {@link ClassBuilderImpl#packageInfos}
     */
    public ClassBuilderImpl(String resultSuffix, Map<String, Set<String>> packageInfos) {
        super();
        this.resultSuffix = resultSuffix;
        this.packageInfos = packageInfos;
    }

    /*
     * (non-Javadoc)
     * 
     * @see vn.universex.builder.ClassBuilder#build(japa.parser.ast.CompilationUnit)
     */
    @Override
    public final ClassBuilderResult build(CompilationUnit n) {
        this.visit(n, null);

        PackageDeclaration packageDeclaration = new PackageDeclaration(ASTHelper.createNameExpr(sourcePackage));
        List<TypeDeclaration> types = Arrays.asList((TypeDeclaration) toBeBuiltClass);
        toBeBuiltCompilationUnit = new CompilationUnit(packageDeclaration, toBeBuiltImports, types, null);

        return new ClassBuilderResult(sourcePackage, toBeBuiltClass.getName(), toBeBuiltCompilationUnit.toString());
    }

    /**
     * Visit package declaration to store current package.
     */
    @Override
    public void visit(PackageDeclaration n, Object arg) {
        sourcePackage = n.getName().toString();
    };

    /**
     * Look at the import of current class to 'know' all imports of current class.
     */
    @Override
    public void visit(ImportDeclaration n, Object arg) {
        if (n.getName() instanceof QualifiedNameExpr) {
            QualifiedNameExpr name = (QualifiedNameExpr) n.getName();
            sourceImports.put(name.getName(), name.getQualifier().toString());
        }
    }

    /**
     * Intercept the class declaration of the old source code and perform create a new class that will be generated to
     * property class.
     */
    @Override
    public void visit(ClassOrInterfaceDeclaration n, Object arg) {
        visit(n);

        super.visit(n, arg);
    }

    public void visit(japa.parser.ast.body.EnumDeclaration n, Object arg) {
        visit(n);

        super.visit(n, arg);
    };

    /**
     * @param n
     */
    protected void visit(TypeDeclaration n) {
        JavadocComment javadoc = new JavadocComment(CLASS_JAVADOC);

        // Build the super class of current property class.
        // We always need the super class that's usually call DefaultProperties to handle some generic behaviors.
        List<ClassOrInterfaceType> toBeBuiltExtendClasses = new ArrayList<>();

        if (n instanceof ClassOrInterfaceDeclaration) {
            ClassOrInterfaceDeclaration ci = (ClassOrInterfaceDeclaration) n;
            List<ClassOrInterfaceType> currentExtendClasses = ci.getExtends();

            // TODO: try another way. I hate myself, why do I have 3 heads and 6 hands? I can do triple jobs. Silly me
            // :)
            if (CollectionUtils.isNotEmpty(currentExtendClasses)) {
                for (ClassOrInterfaceType classOrInterfaceType : currentExtendClasses) {
                    for (Entry<String, Set<String>> e : packageInfos.entrySet()) {
                        if (e.getValue().contains(classOrInterfaceType.getName())) {
                            toBeBuiltExtendClasses.add(new ClassOrInterfaceType(classOrInterfaceType.getName()
                                    + resultSuffix));
                            break;
                        }
                    }
                }
            }
        }

        // After lookup in package information repository, but not found then ... give up? No, used my lovely class as
        // default.
        if (CollectionUtils.isEmpty(toBeBuiltExtendClasses)) {
            toBeBuiltExtendClasses.add(new ClassOrInterfaceType(getDefaultClassName()));
        }

        // Create first 'look and feel' of result class
        String className = n.getName() + resultSuffix;
        toBeBuiltClass = new ClassOrInterfaceDeclaration(javadoc, ModifierSet.PUBLIC, null, false, className, null,
                toBeBuiltExtendClasses, null, null);

        // Build default constructor.
        createDefaultConstructor(className);
    }

    /**
     * Handle create default constructor for the result class.
     * <p>
     * I really like writing the comment in my code, but when I go work, I have no time to do that, I usually put just
     * enough comment on method. I would like to say sorry with all people in my company, though maybe, they never see
     * this comment :).
     * <p>
     * PS: but now I am home, so anything can happen :)). Please careful on my comment :)))
     * 
     * @param className
     *            of course we need the class name to create the constructor, right?
     */
    private void createDefaultConstructor(String className) {
        ConstructorDeclaration constructor = new ConstructorDeclaration(ModifierSet.PUBLIC, className);
        ASTHelper.addMember(toBeBuiltClass, constructor);

        // Add a parameter to the constructor
        // We also always need the parent class for tracing back the property paths.
        addParameter(constructor,
                ASTHelper.createParameter(ASTHelper.createReferenceType(getDefaultClassName(), 0), "parent"));
        addParameter(constructor, ASTHelper.createParameter(ASTHelper.createReferenceType("String", 0), "propertyName"));

        toBeBuiltImports.add(new ImportDeclaration(ASTHelper.createNameExpr(DEFAULT_CLASS_PACKAGE + "."
                + getDefaultClassName()), false, false));

        // Create body of constructor
        BlockStmt block = new BlockStmt();
        constructor.setBlock(block);

        // Add a statement to call super constructor
        ASTHelper.addStmt(block, new NameExpr("super(parent, propertyName)"));
    }

    /**
     * Hook into method visitor to find the getter of all properties. If 1 property is found --> add this one to the
     * result class.
     */
    @Override
    public void visit(MethodDeclaration n, Object arg) {
        Matcher matcher = GETTER_PATTERN.matcher(n.getName());
        if (matcher.matches()) {
            String propertyName = StringUtils.uncapitalize(matcher.group(1));
            addProperty(propertyName, n.getType());
        }
    };

    /**
     * The name of this method is too clear. Oh, maybe addPropertyToResultClass is better :) --> Yeah, refactoring.
     * 
     * @param propertyName
     *            the name of the property to be added.
     * @param type
     *            technical field: the type of this property in the old source code.
     */
    private void addProperty(String propertyName, Type type) {
        type = translateType(type);
        ASTHelper.addMember(
                toBeBuiltClass,
                ASTHelper.createFieldDeclaration(ModifierSet.PUBLIC, type, propertyName + " = new " + type
                        + "(this, \"" + propertyName + "\")"));
    }

    /**
     * Convert the old type to the new type if necessary.
     * 
     * @param type
     *            the type of this property in the old source code.
     * @return
     */
    private Type translateType(Type type) {
        Type t = type;

        if (type instanceof ReferenceType) {
            ReferenceType ref = (ReferenceType) type;
            if (ref.getType() instanceof ClassOrInterfaceType) {
                String className = ref.toString() + resultSuffix;
                ClassOrInterfaceType returnType = (ClassOrInterfaceType) ref.getType();
                if (LIST_OF_COLLECTION_TYPE.contains(returnType.getName()) && returnType.getTypeArgs() != null) {
                    className = returnType.getTypeArgs().get(0).toString() + resultSuffix;
                }

                if (sourceImports.containsKey(returnType.getName())) {
                    String pkg = sourceImports.get(ref.toString());
                    if (packageInfos.containsKey(pkg)) {
                        t = ASTHelper.createReferenceType(className, 0);
                        toBeBuiltImports.add(new ImportDeclaration(ASTHelper.createNameExpr(pkg + "." + className),
                                false, false));
                    } else {
                        t = ASTHelper.createReferenceType(getDefaultClassName(), 0);
                    }
                } else {
                    if (isJavaLangType(type)) {
                        t = ASTHelper.createReferenceType(getDefaultClassName(), 0);
                    } else {
                        if (packageInfos.containsKey(sourcePackage)) {
                            t = ASTHelper.createReferenceType(className, 0);
                        } else {
                            t = ASTHelper.createReferenceType(getDefaultClassName(), 0);
                        }
                    }
                }
                return t;
            }
        }

        return ASTHelper.createReferenceType(getDefaultClassName(), 0);
    }

    // TODO: I want to add the javadoc here, but I have no time to do that, and this screen is too small for me to write
    // the comment ^^, so please wait ... next time
    private String getDefaultClassName() {
        return "Default" + resultSuffix;
    }

    /**
     * Because usually the java.lang is added by default, without importing, so we need this check to know whether the
     * given type is java.lang type or not.
     * 
     * @param type
     *            the to-be-checked type.
     * @return TRUE if this is java.lang type. FALSE if otherwise.
     */
    protected boolean isJavaLangType(Type type) {
        return LIST_OF_JAVA_LANG_TYPE.contains(type.toString());
    }

    /**
     * TODO: javadoc
     * 
     * @param method
     * @param parameter
     */
    public static void addParameter(ConstructorDeclaration method, Parameter parameter) {
        List<Parameter> parameters = method.getParameters();
        if (parameters == null) {
            parameters = new ArrayList<Parameter>();
            method.setParameters(parameters);
        }
        parameters.add(parameter);
    }

}